/*
 * Copyright (c) 2006-2013 Christian Plattner. All rights reserved.
 * Please refer to the LICENSE.txt for licensing details.
 */

package com.trilead.ssh2.channel;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.trilead.ssh2.ChannelCondition;
import com.trilead.ssh2.PacketFormatException;
import com.trilead.ssh2.PacketTypeException;
import com.trilead.ssh2.PtySettings;
import com.trilead.ssh2.ServerConnectionCallback;
import com.trilead.ssh2.ServerSessionCallback;
import com.trilead.ssh2.log.Logger;
import com.trilead.ssh2.packets.*;
import com.trilead.ssh2.server.ServerConnectionState;
import com.trilead.ssh2.transport.MessageHandler;
import com.trilead.ssh2.transport.TransportManager;

/**
 * ChannelManager. Please read the comments in Channel.java.
 * <p/>
 * Besides the crypto part, this is the core of the library.
 *
 * @author Christian Plattner
 * @version $Id: ChannelManager.java 160 2014-05-01 14:30:26Z dkocher@sudo.ch $
 */
public class ChannelManager implements MessageHandler {
    private static final Logger log = Logger.getLogger(ChannelManager.class);

    private final ServerConnectionState server_state;
    private final TransportManager tm;

    private final Map<String, X11ServerData> x11_magic_cookies = new HashMap<String, X11ServerData>();

    private final List<Channel> channels = new ArrayList<Channel>();
    private int nextLocalChannel = 100;
    private boolean shutdown = false;
    private int globalSuccessCounter = 0;
    private int globalFailedCounter = 0;

    private final Map<Integer, RemoteForwardingData> remoteForwardings = new HashMap<Integer, RemoteForwardingData>();

    private final List<IChannelWorkerThread> listenerThreads = new ArrayList<IChannelWorkerThread>();

    private boolean listenerThreadsAllowed = true;

    /**
     * Constructor for client-mode.
     *
     * @param tm
     */
    public ChannelManager(TransportManager tm) {
        this.server_state = null;
        this.tm = tm;
        tm.registerMessageHandler(this, 80, 100);
    }

    /**
     * Constructor for server-mode.
     *
     * @param state
     */
    public ChannelManager(ServerConnectionState state) {
        this.server_state = state;
        this.tm = state.tm;
        tm.registerMessageHandler(this, 80, 100);
    }

    private Channel getChannel(int id) {
        synchronized(channels) {
            for(Channel c : channels) {
                if(c.localID == id) {
                    return c;
                }
            }
        }
        return null;
    }

    private void removeChannel(int id) {
        synchronized(channels) {
            for(Channel c : channels) {
                if(c.localID == id) {
                    channels.remove(c);
                    break;
                }
            }
        }
    }

    private int addChannel(Channel c) {
        synchronized(channels) {
            channels.add(c);
            return nextLocalChannel++;
        }
    }

    private void waitUntilChannelOpen(Channel c) throws IOException {
        synchronized(c) {
            while(c.state == Channel.STATE_OPENING) {
                try {
                    c.wait();
                }
                catch(InterruptedException e) {
                    throw new InterruptedIOException(e.getMessage());
                }
            }

            if(c.state != Channel.STATE_OPEN) {
                removeChannel(c.localID);
                throw c.getReasonClosed();
            }
        }
    }

    private void waitForGlobalSuccessOrFailure() throws IOException {
        synchronized(channels) {
            while((globalSuccessCounter == 0) && (globalFailedCounter == 0)) {
                if(shutdown) {
                    throw new IOException("The connection is being shutdown");
                }

                try {
                    channels.wait();
                }
                catch(InterruptedException e) {
                    throw new InterruptedIOException(e.getMessage());
                }
            }
            if((globalFailedCounter == 0) && (globalSuccessCounter == 1)) {
                return;
            }
            if((globalFailedCounter == 1) && (globalSuccessCounter == 0)) {
                throw new IOException("The server denied the request (did you enable port forwarding?)");
            }
            throw new IOException("Illegal state. The server sent " + globalSuccessCounter
                    + " SSH_MSG_REQUEST_SUCCESS and " + globalFailedCounter + " SSH_MSG_REQUEST_FAILURE messages.");
        }
    }

    private void waitForChannelSuccessOrFailure(Channel c) throws IOException {
        synchronized(c) {
            while((c.successCounter == 0) && (c.failedCounter == 0)) {
                if(c.state != Channel.STATE_OPEN) {
                    throw c.getReasonClosed();
                }
                try {
                    c.wait();
                }
                catch(InterruptedException ignore) {
                    throw new InterruptedIOException();
                }
            }
            if((c.failedCounter == 0) && (c.successCounter == 1)) {
                return;
            }
            if((c.failedCounter == 1) && (c.successCounter == 0)) {
                throw new IOException("The server denied the request.");
            }
            throw new IOException("Illegal state. The server sent " + c.successCounter
                    + " SSH_MSG_CHANNEL_SUCCESS and " + c.failedCounter + " SSH_MSG_CHANNEL_FAILURE messages.");
        }
    }

    public void registerX11Cookie(String hexFakeCookie, X11ServerData data) {
        synchronized(x11_magic_cookies) {
            x11_magic_cookies.put(hexFakeCookie, data);
        }
    }

    public void unRegisterX11Cookie(String hexFakeCookie, boolean killChannels) {
        if(hexFakeCookie == null) {
            throw new IllegalStateException("hexFakeCookie may not be null");
        }

        synchronized(x11_magic_cookies) {
            x11_magic_cookies.remove(hexFakeCookie);
        }

        if(killChannels == false) {
            return;
        }

        log.debug("Closing all X11 channels for the given fake cookie");

        List<Channel> channel_copy = new ArrayList<Channel>();

        synchronized(channels) {
            channel_copy.addAll(channels);
        }

        for(Channel c : channel_copy) {
            synchronized(c) {
                if(hexFakeCookie.equals(c.hexX11FakeCookie) == false) {
                    continue;
                }
            }

            try {
                closeChannel(c, "Closing X11 channel since the corresponding session is closing", true);
            }
            catch(IOException ignored) {
            }
        }
    }

    public X11ServerData checkX11Cookie(String hexFakeCookie) {
        synchronized(x11_magic_cookies) {
            if(hexFakeCookie != null) {
                return x11_magic_cookies.get(hexFakeCookie);
            }
        }
        return null;
    }

    public void closeAllChannels() {
        log.debug("Closing all channels");

        List<Channel> channel_copy = new ArrayList<Channel>();

        synchronized(channels) {
            channel_copy.addAll(channels);
        }

        for(Channel c : channel_copy) {
            try {
                closeChannel(c, "Closing all channels", true);
            }
            catch(IOException ignored) {
            }
        }
    }

    public void closeChannel(Channel c, String reason, boolean force) throws IOException {
        this.closeChannel(c, new ChannelClosedException(reason), force);
    }

    public void closeChannel(Channel c, IOException reason, boolean force) throws IOException {
        byte msg[] = new byte[5];

        synchronized(c) {
            if(force) {
                c.state = Channel.STATE_CLOSED;
                c.EOF = true;
            }

            c.setReasonClosed(reason);

            msg[0] = Packets.SSH_MSG_CHANNEL_CLOSE;
            msg[1] = (byte) (c.remoteID >> 24);
            msg[2] = (byte) (c.remoteID >> 16);
            msg[3] = (byte) (c.remoteID >> 8);
            msg[4] = (byte) (c.remoteID);

            c.notifyAll();
        }

        synchronized(c.channelSendLock) {
            if(c.closeMessageSent) {
                return;
            }
            tm.sendMessage(msg);
            c.closeMessageSent = true;
        }

        log.debug("Sent SSH_MSG_CHANNEL_CLOSE (channel " + c.localID + ")");
    }

    public void sendEOF(Channel c) throws IOException {
        byte[] msg = new byte[5];

        synchronized(c) {
            if(c.state != Channel.STATE_OPEN) {
                return;
            }

            msg[0] = Packets.SSH_MSG_CHANNEL_EOF;
            msg[1] = (byte) (c.remoteID >> 24);
            msg[2] = (byte) (c.remoteID >> 16);
            msg[3] = (byte) (c.remoteID >> 8);
            msg[4] = (byte) (c.remoteID);
        }

        synchronized(c.channelSendLock) {
            if(c.closeMessageSent == true) {
                return;
            }
            tm.sendMessage(msg);
        }


        log.debug("Sent EOF (Channel " + c.localID + "/" + c.remoteID + ")");
    }

    public void sendOpenConfirmation(Channel c) throws IOException {
        PacketChannelOpenConfirmation pcoc = null;

        synchronized(c) {
            if(c.state != Channel.STATE_OPENING) {
                return;
            }

            c.state = Channel.STATE_OPEN;

            pcoc = new PacketChannelOpenConfirmation(c.remoteID, c.localID, c.localWindow, c.localMaxPacketSize);
        }

        synchronized(c.channelSendLock) {
            if(c.closeMessageSent == true) {
                return;
            }
            tm.sendMessage(pcoc.getPayload());
        }
    }

    public void sendData(Channel c, byte[] buffer, int pos, int len) throws IOException {
        while(len > 0) {
            int thislen = 0;
            byte[] msg;

            synchronized(c) {
                while(true) {
                    if(c.state == Channel.STATE_CLOSED) {
                        throw c.getReasonClosed();
                    }
                    if(c.state != Channel.STATE_OPEN) {
                        throw new ChannelClosedException("SSH channel in strange state. (" + c.state + ")");
                    }

                    if(c.remoteWindow != 0) {
                        break;
                    }

                    try {
                        c.wait();
                    }
                    catch(InterruptedException e) {
                        throw new InterruptedIOException(e.getMessage());
                    }
                }

                /* len > 0, no sign extension can happen when comparing */

                thislen = (c.remoteWindow >= len) ? len : (int) c.remoteWindow;

                int estimatedMaxDataLen = c.remoteMaxPacketSize - (tm.getPacketOverheadEstimate() + 9);

                /* The worst case scenario =) a true bottleneck */

                if(estimatedMaxDataLen <= 0) {
                    estimatedMaxDataLen = 1;
                }

                if(thislen > estimatedMaxDataLen) {
                    thislen = estimatedMaxDataLen;
                }

                c.remoteWindow -= thislen;

                msg = new byte[1 + 8 + thislen];

                msg[0] = Packets.SSH_MSG_CHANNEL_DATA;
                msg[1] = (byte) (c.remoteID >> 24);
                msg[2] = (byte) (c.remoteID >> 16);
                msg[3] = (byte) (c.remoteID >> 8);
                msg[4] = (byte) (c.remoteID);
                msg[5] = (byte) (thislen >> 24);
                msg[6] = (byte) (thislen >> 16);
                msg[7] = (byte) (thislen >> 8);
                msg[8] = (byte) (thislen);

                System.arraycopy(buffer, pos, msg, 9, thislen);
            }

            synchronized(c.channelSendLock) {
                if(c.closeMessageSent) {
                    throw c.getReasonClosed();
                }
                tm.sendMessage(msg);
            }

            pos += thislen;
            len -= thislen;
        }
    }

    public int requestGlobalForward(String bindAddress, int bindPort, String targetAddress, int targetPort)
            throws IOException {
        RemoteForwardingData rfd = new RemoteForwardingData();

        rfd.bindAddress = bindAddress;
        rfd.bindPort = bindPort;
        rfd.targetAddress = targetAddress;
        rfd.targetPort = targetPort;

        synchronized(remoteForwardings) {
            if(remoteForwardings.get(bindPort) != null) {
                throw new IOException("There is already a forwarding for remote port " + bindPort);
            }
            remoteForwardings.put(bindPort, rfd);
        }

        synchronized(channels) {
            globalSuccessCounter = globalFailedCounter = 0;
        }

        PacketGlobalForwardRequest pgf = new PacketGlobalForwardRequest(true, bindAddress, bindPort);
        tm.sendMessage(pgf.getPayload());

        log.debug("Requesting a remote forwarding ('" + bindAddress + "', " + bindPort + ")");

        try {
            waitForGlobalSuccessOrFailure();
        }
        catch(IOException e) {
            synchronized(remoteForwardings) {
                remoteForwardings.remove(bindPort);
            }
            throw e;
        }

        return bindPort;
    }

    public void requestCancelGlobalForward(int bindPort) throws IOException {
        RemoteForwardingData rfd;

        synchronized(remoteForwardings) {
            rfd = remoteForwardings.get(bindPort);

            if(rfd == null) {
                throw new IOException("Sorry, there is no known remote forwarding for remote port " + bindPort);
            }
        }

        synchronized(channels) {
            globalSuccessCounter = globalFailedCounter = 0;
        }

        PacketGlobalCancelForwardRequest pgcf = new PacketGlobalCancelForwardRequest(true, rfd.bindAddress,
                rfd.bindPort);
        tm.sendMessage(pgcf.getPayload());

        log.debug("Requesting cancelation of remote forward ('" + rfd.bindAddress + "', " + rfd.bindPort + ")");

        waitForGlobalSuccessOrFailure();

		/* Only now we are sure that no more forwarded connections will arrive */

        synchronized(remoteForwardings) {
            remoteForwardings.remove(bindPort);
        }
    }

    public void registerThread(IChannelWorkerThread thr) throws IOException {
        synchronized(listenerThreads) {
            if(listenerThreadsAllowed == false) {
                throw new IOException("Too late, this connection is closed.");
            }
            listenerThreads.add(thr);
        }
    }

    public Channel openDirectTCPIPChannel(String host_to_connect, int port_to_connect, String originator_IP_address,
                                          int originator_port) throws IOException {
        Channel c = new Channel(this);

        synchronized(c) {
            c.localID = addChannel(c);
            // end of synchronized block forces writing out to main memory
        }

        PacketOpenDirectTCPIPChannel dtc = new PacketOpenDirectTCPIPChannel(c.localID, c.localWindow,
                c.localMaxPacketSize, host_to_connect, port_to_connect, originator_IP_address, originator_port);

        tm.sendMessage(dtc.getPayload());

        waitUntilChannelOpen(c);

        return c;
    }

    public Channel openSessionChannel() throws IOException {
        Channel c = new Channel(this);

        synchronized(c) {
            c.localID = addChannel(c);
            // end of synchronized block forces the writing out to main memory
        }

        log.debug("Sending SSH_MSG_CHANNEL_OPEN (Channel " + c.localID + ")");

        PacketOpenSessionChannel smo = new PacketOpenSessionChannel(c.localID, c.localWindow, c.localMaxPacketSize);
        tm.sendMessage(smo.getPayload());

        waitUntilChannelOpen(c);

        return c;
    }

    public void requestPTY(Channel c, String term, int term_width_characters, int term_height_characters,
                           int term_width_pixels, int term_height_pixels, byte[] terminal_modes) throws IOException {
        PacketSessionPtyRequest spr;

        synchronized(c) {
            if(c.state != Channel.STATE_OPEN) {
                throw c.getReasonClosed();
            }
            spr = new PacketSessionPtyRequest(c.remoteID, true, term, term_width_characters, term_height_characters,
                    term_width_pixels, term_height_pixels, terminal_modes);

            c.successCounter = c.failedCounter = 0;
        }

        synchronized(c.channelSendLock) {
            if(c.closeMessageSent) {
                throw c.getReasonClosed();
            }
            tm.sendMessage(spr.getPayload());
        }

        try {
            waitForChannelSuccessOrFailure(c);
        }
        catch(IOException e) {
            throw new IOException("PTY request failed", e);
        }
    }

    public void requestWindowChange(Channel c, int term_width_characters, int term_height_characters,
                                    int term_width_pixels, int term_height_pixels) throws IOException {
        PacketWindowChange pwc;

        synchronized(c) {
            if(c.state != Channel.STATE_OPEN) {
                throw c.getReasonClosed();
            }
            pwc = new PacketWindowChange(c.remoteID, term_width_characters, term_height_characters,
                    term_width_pixels, term_height_pixels);

            c.successCounter = c.failedCounter = 0;
        }

        synchronized(c.channelSendLock) {
            if(c.closeMessageSent) {
                throw c.getReasonClosed();
            }
            tm.sendMessage(pwc.getPayload());
        }

        try {
            waitForChannelSuccessOrFailure(c);
        }
        catch(IOException e) {
            throw new IOException("The window-change request failed.", e);
        }
    }

    public void requestX11(Channel c, boolean singleConnection, String x11AuthenticationProtocol,
                           String x11AuthenticationCookie, int x11ScreenNumber) throws IOException {
        PacketSessionX11Request psr;

        synchronized(c) {
            if(c.state != Channel.STATE_OPEN) {
                throw c.getReasonClosed();
            }
            psr = new PacketSessionX11Request(c.remoteID, true, singleConnection, x11AuthenticationProtocol,
                    x11AuthenticationCookie, x11ScreenNumber);

            c.successCounter = c.failedCounter = 0;
        }

        synchronized(c.channelSendLock) {
            if(c.closeMessageSent) {
                throw c.getReasonClosed();
            }
            tm.sendMessage(psr.getPayload());
        }

        log.debug("Requesting X11 forwarding (Channel " + c.localID + "/" + c.remoteID + ")");

        try {
            waitForChannelSuccessOrFailure(c);
        }
        catch(IOException e) {
            throw new IOException("The X11 request failed.", e);
        }
    }

    public void requestSubSystem(Channel c, String subSystemName) throws IOException {
        PacketSessionSubsystemRequest ssr;

        synchronized(c) {
            if(c.state != Channel.STATE_OPEN) {
                throw c.getReasonClosed();
            }
            ssr = new PacketSessionSubsystemRequest(c.remoteID, true, subSystemName);

            c.successCounter = c.failedCounter = 0;
        }

        synchronized(c.channelSendLock) {
            if(c.closeMessageSent) {
                throw c.getReasonClosed();
            }
            tm.sendMessage(ssr.getPayload());
        }

        try {
            waitForChannelSuccessOrFailure(c);
        }
        catch(IOException e) {
            throw new IOException("The subsystem request failed.", e);
        }
    }

    public void requestExecCommand(Channel c, String cmd) throws IOException {
        this.requestExecCommand(c, cmd, null);
    }

    /**
     * @param charsetName The charset used to convert between Java Unicode Strings and byte encodings
     */
    public void requestExecCommand(Channel c, String cmd, String charsetName) throws IOException {
        PacketSessionExecCommand sm;

        synchronized(c) {
            if(c.state != Channel.STATE_OPEN) {
                throw c.getReasonClosed();
            }
            sm = new PacketSessionExecCommand(c.remoteID, true, cmd, charsetName);

            c.successCounter = c.failedCounter = 0;
        }

        synchronized(c.channelSendLock) {
            if(c.closeMessageSent) {
                throw c.getReasonClosed();
            }
            tm.sendMessage(sm.getPayload());
        }

        log.debug("Executing command (channel " + c.localID + ", '" + cmd + "')");

        try {
            waitForChannelSuccessOrFailure(c);
        }
        catch(IOException e) {
            throw new IOException("The execute request failed.", e);
        }
    }

    public void requestShell(Channel c) throws IOException {
        PacketSessionStartShell sm;

        synchronized(c) {
            if(c.state != Channel.STATE_OPEN) {
                throw c.getReasonClosed();
            }
            sm = new PacketSessionStartShell(c.remoteID, true);

            c.successCounter = c.failedCounter = 0;
        }

        synchronized(c.channelSendLock) {
            if(c.closeMessageSent) {
                throw c.getReasonClosed();
            }
            tm.sendMessage(sm.getPayload());
        }

        try {
            waitForChannelSuccessOrFailure(c);
        }
        catch(IOException e) {
            throw new IOException("The shell request failed.", e);
        }
    }

    public void msgChannelExtendedData(byte[] msg) throws IOException {
        if(msg.length <= 13) {
            throw new PacketFormatException(String.format("SSH_MSG_CHANNEL_EXTENDED_DATA message has wrong size (%d)", msg.length));
        }

        int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff);
        int dataType = ((msg[5] & 0xff) << 24) | ((msg[6] & 0xff) << 16) | ((msg[7] & 0xff) << 8) | (msg[8] & 0xff);
        int len = ((msg[9] & 0xff) << 24) | ((msg[10] & 0xff) << 16) | ((msg[11] & 0xff) << 8) | (msg[12] & 0xff);

        Channel c = getChannel(id);

        if(c == null) {
            throw new PacketTypeException("Unexpected SSH_MSG_CHANNEL_EXTENDED_DATA message for non-existent channel " + id);
        }

        if(dataType != Packets.SSH_EXTENDED_DATA_STDERR) {
            throw new PacketFormatException("SSH_MSG_CHANNEL_EXTENDED_DATA message has unknown type (" + dataType + ")");
        }

        if(len != (msg.length - 13)) {
            throw new PacketFormatException("SSH_MSG_CHANNEL_EXTENDED_DATA message has wrong len (calculated " + (msg.length - 13)
                    + ", got " + len + ")");
        }

        log.debug("Got SSH_MSG_CHANNEL_EXTENDED_DATA (channel " + id + ", " + len + ")");

        synchronized(c) {
            if(c.state == Channel.STATE_CLOSED) {
                return; // ignore
            }

            if(c.state != Channel.STATE_OPEN) {
                throw new PacketTypeException("Got SSH_MSG_CHANNEL_EXTENDED_DATA, but channel is not in correct state ("
                        + c.state + ")");
            }

            if(c.localWindow < len) {
                throw new PacketFormatException("Remote sent too much data, does not fit into window.");
            }

            c.localWindow -= len;

            System.arraycopy(msg, 13, c.stderrBuffer, c.stderrWritepos, len);
            c.stderrWritepos += len;

            c.notifyAll();
        }
    }

    /**
     * Wait until for a condition.
     *
     * @param c              Channel
     * @param timeout        in ms, 0 means no timeout.
     * @param condition_mask minimum event mask (at least one of the conditions must be fulfilled)
     * @return all current events
     */
    public int waitForCondition(Channel c, long timeout, int condition_mask) throws IOException {
        long end_time = 0;
        boolean end_time_set = false;

        synchronized(c) {
            while(true) {
                int current_cond = 0;

                int stdoutAvail = c.stdoutWritepos - c.stdoutReadpos;
                int stderrAvail = c.stderrWritepos - c.stderrReadpos;

                if(stdoutAvail > 0) {
                    current_cond = current_cond | ChannelCondition.STDOUT_DATA;
                }

                if(stderrAvail > 0) {
                    current_cond = current_cond | ChannelCondition.STDERR_DATA;
                }

                if(c.EOF) {
                    current_cond = current_cond | ChannelCondition.EOF;
                }

                if(c.getExitStatus() != null) {
                    current_cond = current_cond | ChannelCondition.EXIT_STATUS;
                }

                if(c.getExitSignal() != null) {
                    current_cond = current_cond | ChannelCondition.EXIT_SIGNAL;
                }

                if(c.state == Channel.STATE_CLOSED) {
                    return current_cond | ChannelCondition.CLOSED | ChannelCondition.EOF;
                }

                if((current_cond & condition_mask) != 0) {
                    return current_cond;
                }

                if(timeout > 0) {
                    if(!end_time_set) {
                        end_time = System.currentTimeMillis() + timeout;
                        end_time_set = true;
                    }
                    else {
                        timeout = end_time - System.currentTimeMillis();

                        if(timeout <= 0) {
                            return current_cond | ChannelCondition.TIMEOUT;
                        }
                    }
                }

                try {
                    if(timeout > 0) {
                        c.wait(timeout);
                    }
                    else {
                        c.wait();
                    }
                }
                catch(InterruptedException e) {
                    throw new InterruptedIOException(e.getMessage());
                }
            }
        }
    }

    public int getAvailable(Channel c, boolean extended) throws IOException {
        synchronized(c) {
            int avail;

            if(extended) {
                avail = c.stderrWritepos - c.stderrReadpos;
            }
            else {
                avail = c.stdoutWritepos - c.stdoutReadpos;
            }

            return ((avail > 0) ? avail : (c.EOF ? -1 : 0));
        }
    }

    public int getChannelData(Channel c, boolean extended, byte[] target, int off, int len) throws IOException {
        int copylen = 0;
        int increment = 0;
        int remoteID = 0;
        int localID = 0;

        synchronized(c) {
            int stdoutAvail = 0;
            int stderrAvail = 0;

            while(true) {
                /*
                 * Data available? We have to return remaining data even if the
                 * channel is already closed.
                 */

                stdoutAvail = c.stdoutWritepos - c.stdoutReadpos;
                stderrAvail = c.stderrWritepos - c.stderrReadpos;

                if((!extended) && (stdoutAvail != 0)) {
                    break;
                }

                if((extended) && (stderrAvail != 0)) {
                    break;
                }

                /* Do not wait if more data will never arrive (EOF or CLOSED) */

                if((c.EOF) || (c.state != Channel.STATE_OPEN)) {
                    return -1;
                }

                try {
                    c.wait();
                }
                catch(InterruptedException e) {
                    throw new InterruptedIOException(e.getMessage());
                }
            }

            /* OK, there is some data. Return it. */

            if(!extended) {
                copylen = (stdoutAvail > len) ? len : stdoutAvail;
                System.arraycopy(c.stdoutBuffer, c.stdoutReadpos, target, off, copylen);
                c.stdoutReadpos += copylen;

                if(c.stdoutReadpos != c.stdoutWritepos)

                {
                    System.arraycopy(c.stdoutBuffer, c.stdoutReadpos, c.stdoutBuffer, 0, c.stdoutWritepos
                            - c.stdoutReadpos);
                }

                c.stdoutWritepos -= c.stdoutReadpos;
                c.stdoutReadpos = 0;
            }
            else {
                copylen = (stderrAvail > len) ? len : stderrAvail;
                System.arraycopy(c.stderrBuffer, c.stderrReadpos, target, off, copylen);
                c.stderrReadpos += copylen;

                if(c.stderrReadpos != c.stderrWritepos)

                {
                    System.arraycopy(c.stderrBuffer, c.stderrReadpos, c.stderrBuffer, 0, c.stderrWritepos
                            - c.stderrReadpos);
                }

                c.stderrWritepos -= c.stderrReadpos;
                c.stderrReadpos = 0;
            }

            if(c.state != Channel.STATE_OPEN) {
                return copylen;
            }

            if(c.localWindow < ((Channel.CHANNEL_BUFFER_SIZE + 1) / 2)) {
                int minFreeSpace = Math.min(Channel.CHANNEL_BUFFER_SIZE - c.stdoutWritepos,
                        Channel.CHANNEL_BUFFER_SIZE - c.stderrWritepos);

                increment = minFreeSpace - c.localWindow;
                c.localWindow = minFreeSpace;
            }

            remoteID = c.remoteID; /* read while holding the lock */
            localID = c.localID; /* read while holding the lock */
        }

        /*
         * If a consumer reads stdout and stdin in parallel, we may end up with
         * sending two msgWindowAdjust messages. Luckily, it
         * does not matter in which order they arrive at the server.
         */

        if(increment > 0) {
            log.debug("Sending SSH_MSG_CHANNEL_WINDOW_ADJUST (channel " + localID + ", " + increment + ")");

            synchronized(c.channelSendLock) {
                byte[] msg = c.msgWindowAdjust;

                msg[0] = Packets.SSH_MSG_CHANNEL_WINDOW_ADJUST;
                msg[1] = (byte) (remoteID >> 24);
                msg[2] = (byte) (remoteID >> 16);
                msg[3] = (byte) (remoteID >> 8);
                msg[4] = (byte) (remoteID);
                msg[5] = (byte) (increment >> 24);
                msg[6] = (byte) (increment >> 16);
                msg[7] = (byte) (increment >> 8);
                msg[8] = (byte) (increment);

                if(!c.closeMessageSent) {
                    tm.sendMessage(msg);
                }
            }
        }

        return copylen;
    }

    public void msgChannelData(byte[] msg) throws IOException {
        if(msg.length <= 9) {
            throw new PacketFormatException(String.format("SSH_MSG_CHANNEL_DATA message has wrong size (%d)", msg.length));
        }

        int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff);
        int len = ((msg[5] & 0xff) << 24) | ((msg[6] & 0xff) << 16) | ((msg[7] & 0xff) << 8) | (msg[8] & 0xff);

        Channel c = getChannel(id);

        if(c == null) {
            throw new PacketTypeException("Unexpected SSH_MSG_CHANNEL_DATA message for non-existent channel " + id);
        }

        if(len != (msg.length - 9)) {
            throw new PacketFormatException("SSH_MSG_CHANNEL_DATA message has wrong len (calculated " + (msg.length - 9) + ", got "
                    + len + ")");
        }

        log.debug("Got SSH_MSG_CHANNEL_DATA (channel " + id + ", " + len + ")");

        synchronized(c) {
            if(c.state == Channel.STATE_CLOSED) {
                return; // ignore
            }

            if(c.state != Channel.STATE_OPEN) {
                throw new PacketTypeException("Got SSH_MSG_CHANNEL_DATA, but channel is not in correct state (" + c.state + ")");
            }

            if(c.localWindow < len) {
                throw new IOException("Remote sent too much data, does not fit into window.");
            }

            c.localWindow -= len;

            System.arraycopy(msg, 9, c.stdoutBuffer, c.stdoutWritepos, len);
            c.stdoutWritepos += len;

            c.notifyAll();
        }
    }

    public void msgChannelWindowAdjust(byte[] msg) throws IOException {
        if(msg.length != 9) {
            throw new PacketFormatException(String.format("SSH_MSG_CHANNEL_WINDOW_ADJUST message has wrong size (%d)", msg.length));
        }

        int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff);
        int windowChange = ((msg[5] & 0xff) << 24) | ((msg[6] & 0xff) << 16) | ((msg[7] & 0xff) << 8) | (msg[8] & 0xff);

        Channel c = getChannel(id);

        if(c == null) {
            throw new PacketTypeException("Unexpected SSH_MSG_CHANNEL_WINDOW_ADJUST message for non-existent channel " + id);
        }

        synchronized(c) {
            final long huge = 0xFFFFffffL; /* 2^32 - 1 */

            c.remoteWindow += (windowChange & huge); /* avoid sign extension */

			/* TODO - is this a good heuristic? */

            if((c.remoteWindow > huge)) {
                c.remoteWindow = huge;
            }

            c.notifyAll();
        }


        log.debug("Got SSH_MSG_CHANNEL_WINDOW_ADJUST (channel " + id + ", " + windowChange + ")");
    }

    public void msgChannelOpen(byte[] msg) throws IOException {
        TypesReader tr = new TypesReader(msg);

        tr.readByte(); // skip packet type
        String channelType = tr.readString();
        int remoteID = tr.readUINT32(); /* sender channel */
        int remoteWindow = tr.readUINT32(); /* initial window size */
        int remoteMaxPacketSize = tr.readUINT32(); /* maximum packet size */

        if("x11".equals(channelType)) {
            synchronized(x11_magic_cookies) {
                /* If we did not request X11 forwarding, then simply ignore this bogus request. */

                if(x11_magic_cookies.size() == 0) {
                    PacketChannelOpenFailure pcof = new PacketChannelOpenFailure(remoteID,
                            Packets.SSH_OPEN_ADMINISTRATIVELY_PROHIBITED, "X11 forwarding not activated", "");

                    tm.sendAsynchronousMessage(pcof.getPayload());

                    log.warning("Unexpected X11 request, denying it!");

                    return;
                }
            }

            String remoteOriginatorAddress = tr.readString();
            int remoteOriginatorPort = tr.readUINT32();

            Channel c = new Channel(this);

            synchronized(c) {
                c.remoteID = remoteID;
                c.remoteWindow = remoteWindow & 0xFFFFffffL; /* properly convert UINT32 to long */
                c.remoteMaxPacketSize = remoteMaxPacketSize;
                c.localID = addChannel(c);
            }

			/*
             * The open confirmation message will be sent from another thread
			 */

            RemoteX11AcceptThread rxat = new RemoteX11AcceptThread(c, remoteOriginatorAddress, remoteOriginatorPort);
            rxat.setDaemon(true);
            rxat.start();

            return;
        }

        if("forwarded-tcpip".equals(channelType)) {
            String remoteConnectedAddress = tr.readString(); /* address that was connected */
            int remoteConnectedPort = tr.readUINT32(); /* port that was connected */
            String remoteOriginatorAddress = tr.readString(); /* originator IP address */
            int remoteOriginatorPort = tr.readUINT32(); /* originator port */

            RemoteForwardingData rfd;

            synchronized(remoteForwardings) {
                rfd = remoteForwardings.get(remoteConnectedPort);
            }

            if(rfd == null) {
                PacketChannelOpenFailure pcof = new PacketChannelOpenFailure(remoteID,
                        Packets.SSH_OPEN_ADMINISTRATIVELY_PROHIBITED,
                        "No thanks, unknown port in forwarded-tcpip request", "");

				/* Always try to be polite. */

                tm.sendAsynchronousMessage(pcof.getPayload());

                log.debug("Unexpected forwarded-tcpip request, denying it!");

                return;
            }

            Channel c = new Channel(this);

            synchronized(c) {
                c.remoteID = remoteID;
                c.remoteWindow = remoteWindow & 0xFFFFffffL; /* convert UINT32 to long */
                c.remoteMaxPacketSize = remoteMaxPacketSize;
                c.localID = addChannel(c);
            }

			/*
			 * The open confirmation message will be sent from another thread.
			 */

            RemoteAcceptThread rat = new RemoteAcceptThread(c, remoteConnectedAddress, remoteConnectedPort,
                    remoteOriginatorAddress, remoteOriginatorPort, rfd.targetAddress, rfd.targetPort);

            rat.setDaemon(true);
            rat.start();

            return;
        }

        if((server_state != null) && ("session".equals(channelType))) {
            ServerConnectionCallback cb;

            synchronized(server_state) {
                cb = server_state.cb_conn;
            }

            if(cb == null) {
                tm.sendAsynchronousMessage(new PacketChannelOpenFailure(remoteID, Packets.SSH_OPEN_ADMINISTRATIVELY_PROHIBITED,
                        "Sessions are currently not enabled", "en").getPayload());

                return;
            }

            final Channel c = new Channel(this);

            synchronized(c) {
                c.remoteID = remoteID;
                c.remoteWindow = remoteWindow & 0xFFFFffffL; /* convert UINT32 to long */
                c.remoteMaxPacketSize = remoteMaxPacketSize;
                c.localID = addChannel(c);
                c.state = Channel.STATE_OPEN;
                c.ss = new ServerSessionImpl(c);
            }

            PacketChannelOpenConfirmation pcoc = new PacketChannelOpenConfirmation(c.remoteID, c.localID,
                    c.localWindow, c.localMaxPacketSize);

            tm.sendAsynchronousMessage(pcoc.getPayload());

            c.ss.sscb = cb.acceptSession(c.ss);

            return;
        }

		/* Tell the server that we have no idea what it is talking about */

        PacketChannelOpenFailure pcof = new PacketChannelOpenFailure(remoteID, Packets.SSH_OPEN_UNKNOWN_CHANNEL_TYPE,
                "Unknown channel type", "");

        tm.sendAsynchronousMessage(pcof.getPayload());


        log.warning("The peer tried to open an unsupported channel type (" + channelType + ")");
    }

    /* Starts the given runnable in a foreground (non-daemon) thread */
    private void runAsync(Runnable r) {
        Thread t = new Thread(r);
        t.start();
    }

    public void msgChannelRequest(byte[] msg) throws IOException {
        TypesReader tr = new TypesReader(msg);

        tr.readByte(); // skip packet type
        int id = tr.readUINT32();

        Channel c = getChannel(id);

        if(c == null) {
            throw new IOException("Unexpected SSH_MSG_CHANNEL_REQUEST message for non-existent channel " + id);
        }

        ServerSessionImpl server_session = null;

        if(server_state != null) {
            synchronized(c) {
                server_session = c.ss;
            }
        }

        String type = tr.readString("US-ASCII");
        boolean wantReply = tr.readBoolean();

        log.debug("Got SSH_MSG_CHANNEL_REQUEST (channel " + id + ", '" + type + "')");

        if(type.equals("exit-status")) {
            if(wantReply) {
                throw new IOException(
                        "Badly formatted SSH_MSG_CHANNEL_REQUEST exit-status message, 'want reply' is true");
            }

            int exit_status = tr.readUINT32();

            if(tr.remain() != 0) {
                throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message");
            }

            synchronized(c) {
                c.exit_status = exit_status;
                c.notifyAll();
            }

            log.debug("Got EXIT STATUS (channel " + id + ", status " + exit_status + ")");

            return;
        }

        if((server_state == null) && (type.equals("exit-signal"))) {
            if(wantReply) {
                throw new IOException(
                        "Badly formatted SSH_MSG_CHANNEL_REQUEST exit-signal message, 'want reply' is true");
            }

            String signame = tr.readString("US-ASCII");
            tr.readBoolean();
            tr.readString();
            tr.readString();

            if(tr.remain() != 0) {
                throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message");
            }

            synchronized(c) {
                c.exit_signal = signame;
                c.notifyAll();
            }

            log.debug("Got EXIT SIGNAL (channel " + id + ", signal " + signame + ")");

            return;
        }

        if((server_session != null) && (type.equals("pty-req"))) {
            PtySettings pty = new PtySettings();

            pty.term = tr.readString();
            pty.term_width_characters = tr.readUINT32();
            pty.term_height_characters = tr.readUINT32();
            pty.term_width_pixels = tr.readUINT32();
            pty.term_height_pixels = tr.readUINT32();
            pty.terminal_modes = tr.readByteString();

            if(tr.remain() != 0) {
                throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message");
            }

            Runnable run_after_sending_success = null;

            ServerSessionCallback sscb = server_session.getServerSessionCallback();

            if(sscb != null) {
                run_after_sending_success = sscb.requestPtyReq(server_session, pty);
            }

            if(wantReply) {
                if(run_after_sending_success != null) {
                    tm.sendAsynchronousMessage(new PacketChannelSuccess(c.remoteID).getPayload());
                }
                else {
                    tm.sendAsynchronousMessage(new PacketChannelFailure(c.remoteID).getPayload());
                }
            }

            if(run_after_sending_success != null) {
                runAsync(run_after_sending_success);
            }

            return;
        }

        if((server_session != null) && (type.equals("shell"))) {
            if(tr.remain() != 0) {
                throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message");
            }

            Runnable run_after_sending_success = null;
            ServerSessionCallback sscb = server_session.getServerSessionCallback();

            if(sscb != null) {
                run_after_sending_success = sscb.requestShell(server_session);
            }

            if(wantReply) {
                if(run_after_sending_success != null) {
                    tm.sendAsynchronousMessage(new PacketChannelSuccess(c.remoteID).getPayload());
                }
                else {
                    tm.sendAsynchronousMessage(new PacketChannelFailure(c.remoteID).getPayload());
                }
            }

            if(run_after_sending_success != null) {
                runAsync(run_after_sending_success);
            }

            return;
        }

        if((server_session != null) && (type.equals("exec"))) {
            String command = tr.readString();

            if(tr.remain() != 0) {
                throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message");
            }

            Runnable run_after_sending_success = null;
            ServerSessionCallback sscb = server_session.getServerSessionCallback();

            if(sscb != null) {
                run_after_sending_success = sscb.requestExec(server_session, command);
            }

            if(wantReply) {
                if(run_after_sending_success != null) {
                    tm.sendAsynchronousMessage(new PacketChannelSuccess(c.remoteID).getPayload());
                }
                else {
                    tm.sendAsynchronousMessage(new PacketChannelFailure(c.remoteID).getPayload());
                }
            }

            if(run_after_sending_success != null) {
                runAsync(run_after_sending_success);
            }

            return;
        }

		/* We simply ignore unknown channel requests, however, if the server wants a reply,
		 * then we signal that we have no idea what it is about.
		 */

        if(wantReply) {
            tm.sendAsynchronousMessage(new PacketChannelFailure(c.remoteID).getPayload());
        }

        log.debug("Channel request '" + type + "' is not known, ignoring it");
    }

    public void msgChannelEOF(byte[] msg) throws IOException {
        if(msg.length != 5) {
            throw new PacketFormatException(String.format("SSH_MSG_CHANNEL_EOF message has wrong size (%d)", msg.length));
        }

        int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff);

        Channel c = getChannel(id);

        if(c == null) {
            throw new PacketTypeException("Unexpected SSH_MSG_CHANNEL_EOF message for non-existent channel " + id);
        }

        synchronized(c) {
            c.EOF = true;
            c.notifyAll();
        }

        log.debug("Got SSH_MSG_CHANNEL_EOF (channel " + id + ")");
    }

    public void msgChannelClose(byte[] msg) throws IOException {
        if(msg.length != 5) {
            throw new PacketFormatException("SSH_MSG_CHANNEL_CLOSE message has wrong size (" + msg.length + ")");
        }

        int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff);

        Channel c = getChannel(id);

        if(c == null) {
            throw new IOException("Unexpected SSH_MSG_CHANNEL_CLOSE message for non-existent channel " + id);
        }

        synchronized(c) {
            c.EOF = true;
            c.state = Channel.STATE_CLOSED;
            c.setReasonClosed(new ChannelClosedException("Close requested by remote"));
            c.closeMessageRecv = true;

            removeChannel(c.localID);

            c.notifyAll();
        }

        log.debug("Got SSH_MSG_CHANNEL_CLOSE (channel " + id + ")");
    }

    public void msgChannelSuccess(byte[] msg) throws IOException {
        if(msg.length != 5) {
            throw new PacketFormatException("SSH_MSG_CHANNEL_SUCCESS message has wrong size (" + msg.length + ")");
        }

        int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff);

        Channel c = getChannel(id);

        if(c == null) {
            throw new PacketTypeException("Unexpected SSH_MSG_CHANNEL_SUCCESS message for non-existent channel " + id);
        }

        synchronized(c) {
            c.successCounter++;
            c.notifyAll();
        }

        log.debug("Got SSH_MSG_CHANNEL_SUCCESS (channel " + id + ")");
    }

    public void msgChannelFailure(byte[] msg) throws IOException {
        if(msg.length != 5) {
            throw new PacketFormatException(String.format("SSH_MSG_CHANNEL_FAILURE message has wrong size (%d)", msg.length));
        }

        int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff);

        Channel c = getChannel(id);

        if(c == null) {
            throw new PacketTypeException("Unexpected SSH_MSG_CHANNEL_FAILURE message for non-existent channel " + id);
        }

        synchronized(c) {
            c.failedCounter++;
            c.notifyAll();
        }

        log.debug("Got SSH_MSG_CHANNEL_FAILURE (channel " + id + ")");
    }

    public void msgChannelOpenConfirmation(byte[] msg) throws IOException {
        PacketChannelOpenConfirmation sm = new PacketChannelOpenConfirmation(msg);

        Channel c = getChannel(sm.getRecipientChannelID());

        if(c == null) {
            throw new PacketTypeException("Unexpected SSH_MSG_CHANNEL_OPEN_CONFIRMATION message for non-existent channel "
                    + sm.getRecipientChannelID());
        }

        synchronized(c) {
            if(c.state != Channel.STATE_OPENING) {
                throw new PacketTypeException("Unexpected SSH_MSG_CHANNEL_OPEN_CONFIRMATION message for channel "
                        + sm.getRecipientChannelID());
            }

            c.remoteID = sm.getSenderChannelID();
            c.remoteWindow = sm.getInitialWindowSize() & 0xFFFFffffL; /* convert UINT32 to long */
            c.remoteMaxPacketSize = sm.getMaxPacketSize();
            c.state = Channel.STATE_OPEN;
            c.notifyAll();
        }

        log.debug("Got SSH_MSG_CHANNEL_OPEN_CONFIRMATION (channel " + sm.getRecipientChannelID() + " / remote: "
                + sm.getSenderChannelID() + ")");
    }

    public void msgChannelOpenFailure(byte[] msg) throws IOException {
        if(msg.length < 5) {
            throw new PacketFormatException(String.format("SSH_MSG_CHANNEL_OPEN_FAILURE message has wrong size (%d)", msg.length));
        }

        TypesReader tr = new TypesReader(msg);

        tr.readByte(); // skip packet type
        int id = tr.readUINT32(); /* sender channel */

        Channel c = getChannel(id);

        if(c == null) {
            throw new PacketTypeException("Unexpected SSH_MSG_CHANNEL_OPEN_FAILURE message for non-existent channel " + id);
        }

        int reasonCode = tr.readUINT32();
        String description = tr.readString("UTF-8");

        String reasonCodeSymbolicName;

        switch(reasonCode) {
            case 1:
                reasonCodeSymbolicName = "SSH_OPEN_ADMINISTRATIVELY_PROHIBITED";
                break;
            case 2:
                reasonCodeSymbolicName = "SSH_OPEN_CONNECT_FAILED";
                break;
            case 3:
                reasonCodeSymbolicName = "SSH_OPEN_UNKNOWN_CHANNEL_TYPE";
                break;
            case 4:
                reasonCodeSymbolicName = "SSH_OPEN_RESOURCE_SHORTAGE";
                break;
            default:
                reasonCodeSymbolicName = "UNKNOWN REASON CODE (" + reasonCode + ")";
        }

        StringBuilder descriptionBuffer = new StringBuilder();
        descriptionBuffer.append(description);

        for(int i = 0; i < descriptionBuffer.length(); i++) {
            char cc = descriptionBuffer.charAt(i);

            if((cc >= 32) && (cc <= 126)) {
                continue;
            }
            descriptionBuffer.setCharAt(i, '\uFFFD');
        }

        synchronized(c) {
            c.EOF = true;
            c.state = Channel.STATE_CLOSED;
            c.setReasonClosed(new ChannelClosedException(String.format("The server refused to open the channel (%s, '%s')",
                    reasonCodeSymbolicName, descriptionBuffer.toString())));
            c.notifyAll();
        }

        log.debug("Got SSH_MSG_CHANNEL_OPEN_FAILURE (channel " + id + ")");
    }

    public void msgGlobalRequest(byte[] msg) throws IOException {
		/* Currently we do not support any kind of global request */

        TypesReader tr = new TypesReader(msg);

        tr.readByte(); // skip packet type
        String requestName = tr.readString();
        boolean wantReply = tr.readBoolean();

        if(wantReply) {
            byte[] reply_failure = new byte[1];
            reply_failure[0] = Packets.SSH_MSG_REQUEST_FAILURE;

            tm.sendAsynchronousMessage(reply_failure);
        }

		/* We do not clean up the requestName String - that is OK for debug */

        log.debug("Got SSH_MSG_GLOBAL_REQUEST (" + requestName + ")");
    }

    public void msgGlobalSuccess() throws IOException {
        synchronized(channels) {
            globalSuccessCounter++;
            channels.notifyAll();
        }

        log.debug("Got SSH_MSG_REQUEST_SUCCESS");
    }

    public void msgGlobalFailure() throws IOException {
        synchronized(channels) {
            globalFailedCounter++;
            channels.notifyAll();
        }

        log.debug("Got SSH_MSG_REQUEST_FAILURE");
    }

    @Override
    public void handleFailure(final IOException failure) {
        log.debug("HandleMessage: got shutdown");

        synchronized(listenerThreads) {
            for(IChannelWorkerThread lat : listenerThreads) {
                lat.stopWorking();
            }
            listenerThreadsAllowed = false;
        }

        synchronized(channels) {
            shutdown = true;

            for(Channel c : channels) {
                synchronized(c) {
                    c.EOF = true;
                    c.state = Channel.STATE_CLOSED;
                    c.setReasonClosed(failure);
                    c.closeMessageRecv = true;
                    c.notifyAll();
                }
            }
            channels.clear();
            channels.notifyAll(); /* Notify global response waiters */
        }
    }

    public void handleMessage(byte[] msg) throws IOException {
        switch(msg[0]) {
            case Packets.SSH_MSG_CHANNEL_OPEN_CONFIRMATION:
                msgChannelOpenConfirmation(msg);
                break;
            case Packets.SSH_MSG_CHANNEL_WINDOW_ADJUST:
                msgChannelWindowAdjust(msg);
                break;
            case Packets.SSH_MSG_CHANNEL_DATA:
                msgChannelData(msg);
                break;
            case Packets.SSH_MSG_CHANNEL_EXTENDED_DATA:
                msgChannelExtendedData(msg);
                break;
            case Packets.SSH_MSG_CHANNEL_REQUEST:
                msgChannelRequest(msg);
                break;
            case Packets.SSH_MSG_CHANNEL_EOF:
                msgChannelEOF(msg);
                break;
            case Packets.SSH_MSG_CHANNEL_OPEN:
                msgChannelOpen(msg);
                break;
            case Packets.SSH_MSG_CHANNEL_CLOSE:
                msgChannelClose(msg);
                break;
            case Packets.SSH_MSG_CHANNEL_SUCCESS:
                msgChannelSuccess(msg);
                break;
            case Packets.SSH_MSG_CHANNEL_FAILURE:
                msgChannelFailure(msg);
                break;
            case Packets.SSH_MSG_CHANNEL_OPEN_FAILURE:
                msgChannelOpenFailure(msg);
                break;
            case Packets.SSH_MSG_GLOBAL_REQUEST:
                msgGlobalRequest(msg);
                break;
            case Packets.SSH_MSG_REQUEST_SUCCESS:
                msgGlobalSuccess();
                break;
            case Packets.SSH_MSG_REQUEST_FAILURE:
                msgGlobalFailure();
                break;
            default:
                throw new PacketTypeException(msg[0]);
        }
    }
}
